package org.xenei.contracts.maven; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.IOUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.apache.maven.repository.RepositorySystem; import org.codehaus.plexus.classworlds.ClassWorld; import org.codehaus.plexus.classworlds.realm.ClassRealm; import org.codehaus.plexus.classworlds.realm.DuplicateRealmException; import org.codehaus.plexus.util.StringUtils; import org.xenei.classpathutils.ClassPathFilter; import org.xenei.classpathutils.ClassPathUtils; import org.xenei.classpathutils.filter.NotClassFilter; import org.xenei.classpathutils.filter.parser.Parser; import org.xenei.junit.contract.Contract; import org.xenei.junit.contract.ContractImpl; import org.xenei.junit.contract.NoContractTest; import org.xenei.junit.contract.tooling.InterfaceInfo; import org.xenei.junit.contract.tooling.InterfaceReport; /** * Generate contract test reports. * */ @Mojo(name = "contract-test", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES, requiresDependencyResolution = ResolutionScope.TEST) public class ContractMojo extends AbstractMojo { /** * A list of packages to process. Includes sub packages. */ @Parameter private String[] packages; /** * A filter expression for classes to skip. */ @Parameter() private String skipFilter; /** * The filter generated by the above string. This is the filter of classes * to keep expressed as NOT( filterString ). */ private ClassPathFilter filter = ClassPathFilter.TRUE; /** * Report configuration for untested interfaces. Untested interfaces are * interfaces that are defined in the list of packages but that do not have * contract tests and are not annotated with NoContractTest. */ @Parameter private ReportConfig untested; /** * Report configuration for unimplemented tests. Unimplemented tests are * classes that implement an interface that has a Contract test but for * which no contract suite test implementation is found. */ @Parameter private ReportConfig unimplemented; /** * If there are errors in the ContractMojo should the build be failed. * Defaults to true; */ @Parameter private boolean failOnError = true; /** * Report configuration for errors generated during run. */ @Parameter private ReportConfig errors; @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true) private File classDir; @Parameter(defaultValue = "${project.build.testOutputDirectory}", readonly = true) private File testDir; @Parameter(defaultValue = "${project.build.directory}", readonly = true) private File target; @Component private MavenProject project; @Parameter(defaultValue = "${plugin.artifactMap}", required = true, readonly = true) private Map<String, Artifact> pluginArtifactMap; @Component private RepositorySystem repositorySystem; @Parameter(defaultValue = "${localRepository}", required = true, readonly = true) private ArtifactRepository localRepository; private Set<Artifact> junitContractsArtifacts; private File myDir; private final StringBuilder failureMessage = new StringBuilder(); public ContractMojo() { } public void setPackages(final String[] packages) { this.packages = packages; } public void setSkipFilter(final String filter) throws MojoExecutionException { if (StringUtils.isBlank(filter)) { this.filter = ClassPathFilter.TRUE; } else { try { this.filter = new NotClassFilter(new Parser().parse(filter)); } catch (IllegalArgumentException e) { throw new MojoExecutionException(String.format( "Could not create parse filter: %s", filter, e.getMessage()), e); } } } public void setErrors(final ReportConfig errors) { this.errors = errors; } public void setUntested(ReportConfig untested) { this.untested = untested; } public void setUnimplemented(ReportConfig unimplemented) { this.unimplemented = unimplemented; } /** * If true the build will fail if there is an error in the mojo. Defaults to * <code>true</code> * * @param failOnError * if true the build will fail on error. */ public void setFailOnError(boolean failOnError) { this.failOnError = failOnError; } private void mojoError( String err ) throws MojoExecutionException { if (failOnError) { getLog().error(err); throw new MojoExecutionException(err); } getLog().info(err); } private void mojoError( String err, Throwable throwable ) throws MojoExecutionException { if (failOnError) { getLog().error(err, throwable); throw new MojoExecutionException(err, throwable); } getLog().info(err, throwable); } @Override public void execute() throws MojoExecutionException { boolean success = true; try { if ((packages == null) || (packages.length == 0)) { mojoError( "At least one package must be specified"); return; } if (getLog().isInfoEnabled()) { for (final String s : packages) { getLog().info("Processing package: " + s); } getLog().info("Skip filter: " + filter); } myDir = new File(target, "contract-reports"); if (!myDir.exists()) { myDir.mkdirs(); } InterfaceReport ir; try { ir = new InterfaceReport(packages, filter, buildClassLoader()); } catch (IllegalArgumentException e1) { mojoError("Could not create Interface report class", e1); return; } doReportInterfaces(ir); success &= doReportUntested(ir.getUntestedInterfaces()); success &= doReportUnimplemented(ir.getUnImplementedTests()); success &= doReportErrors(ir.getErrors()); if (!success) { mojoError(failureMessage.toString()); } } catch (RuntimeException e) { mojoError(e.getMessage(), e); } } private void addFailureMessage(final String msg) { addFailureMessage(msg, null); } private void addFailureMessage(final String msg, final Exception e) { if (failureMessage.length() > 0) { failureMessage.append(System.getProperty("line.separator")); } if (e == null) { getLog().warn(msg); } else { getLog().warn(msg, e); } failureMessage.append(msg); } private void doReportInterfaces(final InterfaceReport ir) { BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter(new File(myDir, "interfaces.txt"))); bw.write(String.format("Filter: %s", ir.getClassFilter())); bw.newLine(); bw.newLine(); bw.write("A list of all interfaces that meet the filter and their contract tests"); bw.newLine(); bw.write("----------------------------------------------------------------------"); bw.newLine(); bw.newLine(); for (final InterfaceInfo ii : ir.getInterfaceInfoCollection()) { if (ir.getClassFilter().accept(ii.getName())) { final String entry = String.format("Interface: %s %s", ii .getName().getName(), ii.getTests()); if (getLog().isDebugEnabled()) { getLog().debug(entry); } bw.write(entry); bw.newLine(); } } bw.newLine(); bw.write("A list of all classes that meet the filter"); bw.newLine(); bw.write("------------------------------------------"); bw.newLine(); bw.newLine(); for (final Class<?> cls : ir.getClassFilter().filterClasses( ir.getPackageClasses())) { final String entry = String.format( "Class: %s, contract: %s, impl: %s, flg: %s, all: %s", cls.getName(), cls.getAnnotation(Contract.class) != null, cls.getAnnotation(ContractImpl.class) != null, cls.getAnnotation(NoContractTest.class) != null, Arrays.asList(cls.getAnnotations())); if (getLog().isDebugEnabled()) { getLog().debug(entry); } bw.write(entry); bw.newLine(); } } catch (final IOException e) { getLog().warn(e.getMessage(), e); } finally { IOUtils.closeQuietly(bw); } } private boolean doReportUntested(final Set<Class<?>> untestedInterfaces) { if (!untestedInterfaces.isEmpty()) { if (untested.isReporting()) { BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter(new File(myDir, "untested.txt"))); bw.write("Interfaces that are defined in the list of packages but that"); bw.newLine(); bw.write("do not have contract tests and are not annotated with NoContractTest."); bw.newLine(); bw.write("---------------------------------------------------------------------"); bw.newLine(); bw.newLine(); bw.write(String.format("Filter: %s", untested.getFilter())); bw.newLine(); bw.newLine(); for (final Class<?> c : untested.getFilter().filterClasses( untestedInterfaces)) { bw.write(c.getName()); bw.newLine(); } } catch (final IOException e) { addFailureMessage("Unable to write untested report", e); return false; } finally { IOUtils.closeQuietly(bw); } } if (untested.isFailOnError() && !untested.getFilter().filterClasses(untestedInterfaces) .isEmpty()) { addFailureMessage("Untested Interfaces Exist"); return false; } } return true; } private boolean doReportUnimplemented(final Set<Class<?>> unimplementedTests) { if (!unimplementedTests.isEmpty()) { if (unimplemented.isReporting()) { BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter(new File(myDir, "unimplemented.txt"))); bw.write("Classes that implement an interface that has a Contract test"); bw.newLine(); bw.write("but for which no contract suite test implementation is found."); bw.newLine(); bw.write("-------------------------------------------------------------"); bw.newLine(); bw.newLine(); bw.write(String.format("Filter: %s", unimplemented.getFilter())); bw.newLine(); bw.newLine(); for (final Class<?> c : unimplemented.getFilter().filterClasses( unimplementedTests)) { bw.write(c.getName()); bw.newLine(); } } catch (final IOException e) { addFailureMessage("Unable to write unimplemented report", e); return false; } finally { IOUtils.closeQuietly(bw); } } if (unimplemented.isFailOnError() && !unimplemented.getFilter().filterClasses(unimplementedTests) .isEmpty()) { addFailureMessage("Unimplemented Tests Exist"); return false; } } return true; } private boolean doReportErrors(final List<Throwable> errorLst) { if (!errorLst.isEmpty()) { if (errors.isReporting()) { BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter(new File(myDir, "errors.txt"))); for (final Throwable t : errorLst) { bw.write(t.toString()); bw.newLine(); } } catch (final IOException e) { addFailureMessage("Unable to write error report", e); return false; } finally { IOUtils.closeQuietly(bw); } } if (errors.isFailOnError()) { addFailureMessage("Contract Test Errors Exist"); return false; } } return true; } private ClassLoader buildClassLoader() throws MojoExecutionException { final ClassWorld world = new ClassWorld(); ClassRealm realm; try { realm = world.newRealm("contract", null); // add contract test and it's transient dependencies. for (final Artifact elt : getJunitContractsArtifacts()) { final String dir = String.format("%s!/", elt.getFile().toURI() .toURL()); if (getLog().isDebugEnabled()) { getLog().debug("Checking for imports from: " + dir); } try { final Set<String> classNames = ClassPathUtils.findClasses( dir, "org.xenei.junit.contract"); for (final String clsName : classNames) { if (getLog().isDebugEnabled()) { getLog().debug( "Importing from current classloader: " + clsName); } importFromCurrentClassLoader(realm, Class.forName(clsName)); } } catch (final ClassNotFoundException e) { throw new MojoExecutionException(e.toString(), e); } catch (final IOException e) { throw new MojoExecutionException(e.toString(), e); } } // add source dirs for (final String elt : project.getCompileSourceRoots()) { final URL url = new File(elt).toURI().toURL(); realm.addURL(url); if (getLog().isDebugEnabled()) { getLog().debug("Source root: " + url); } } // add Compile classpath for (final String elt : project.getCompileClasspathElements()) { final URL url = new File(elt).toURI().toURL(); realm.addURL(url); if (getLog().isDebugEnabled()) { getLog().debug("Compile classpath: " + url); } } // add Test classpath for (final String elt : project.getTestClasspathElements()) { final URL url = new File(elt).toURI().toURL(); realm.addURL(url); if (getLog().isDebugEnabled()) { getLog().debug("Test classpath: " + url); } } } catch (final DuplicateRealmException e) { throw new MojoExecutionException(e.getMessage(), e); } catch (final MalformedURLException e) { throw new MojoExecutionException(e.getMessage(), e); } catch (final DependencyResolutionRequiredException e) { throw new MojoExecutionException(e.getMessage(), e); } return realm; } private void importFromCurrentClassLoader(final ClassRealm realm, final Class<?> cls) { if (cls == null) { return; } realm.importFrom(Thread.currentThread().getContextClassLoader(), cls.getName()); // ClassRealm importing is prefix-based, so no need to specifically add // inner classes for (final Class<?> intf : cls.getInterfaces()) { importFromCurrentClassLoader(realm, intf); } importFromCurrentClassLoader(realm, cls.getSuperclass()); } private Set<Artifact> getJunitContractsArtifacts() { if (junitContractsArtifacts == null) { final ArtifactResolutionRequest request = new ArtifactResolutionRequest() .setArtifact( pluginArtifactMap.get("org.xenei:junit-contracts")) .setResolveTransitively(true) .setLocalRepository(localRepository); final ArtifactResolutionResult result = repositorySystem .resolve(request); junitContractsArtifacts = result.getArtifacts(); } return junitContractsArtifacts; } }